/**
 * 判断是否 Record 类型，当指定类型 `T` 时，并进行类型推断，当结果为真时，参数 `o` 会被推断为 `T`
 *
 * @param o
 */
import { i18n } from './i18n';
import { isNumeric } from './numeric';
import { isString } from './string';

// eslint-disable-next-line @typescript-eslint/ban-types
export type AnyObject = {};

export const isRecord = <T = AnyObject>(o: unknown): o is T =>
  o != null && typeof o === 'object' && !Array.isArray(o);

export enum ValidateCode {
  null = -11,
  undefined = -10,
  typeError = -1,
  negative = 0,
  positive = 1,
}

export type ValidateResult = {
  code: ValidateCode;
  err?: Error;
};

export type FieldSimpleType = 'string' | 'number' | 'boolean';
export type FieldValidateResult = Error | ValidateCode | boolean;
export type FieldValidateCallback = (v: unknown, f: string) => FieldValidateResult;

export type ValidationsStringTypeArray<T = AnyObject> = Array<keyof T | string>;
export type ValidationsMap<T = AnyObject> = {
  [key in keyof T]?: FieldSimpleType | FieldValidateCallback;
} & {
  [key: string]: FieldSimpleType | FieldValidateCallback;
};

const convertValidations = <T = AnyObject>(
  validations: ValidationsStringTypeArray<T> | ValidationsMap<T>,
): ValidationsMap<T> => {
  if (Array.isArray(validations)) {
    return validations.reduce((prev, v) => {
      // @ts-ignore prev[v]
      prev[v] = 'string';
      return prev;
    }, {} as ValidationsMap<T>);
  }
  return validations as ValidationsMap<T>;
};

const convertValidateResult = (res: FieldValidateResult): ValidateResult => {
  if (res instanceof Error) return { code: ValidateCode.negative, err: res };
  const code = typeof res === 'boolean' ? (res ? ValidateCode.positive : ValidateCode.negative) : res;
  return { code };
};

const validate = (field: string, value: unknown, type?: FieldSimpleType | FieldValidateCallback): ValidateResult => {
  let res: FieldValidateResult = ValidateCode.positive;
  switch (type) {
    case 'string':
      res = isString(value);
      break;
    case 'number':
      res = isNumeric(value);
      break;
    case 'boolean':
      res = typeof value === 'boolean';
      break;
    default:
      if (typeof type === 'function') {
        try {
          res = type(value, field);
        } catch (e) {
          res = e instanceof Error ? e : false;
        }
      }
  }
  return convertValidateResult(res);
};

/**
 * 验证错误类型
 */
export class ValidationError extends Error {
  constructor(message?: string, public fields: Record<string, ValidateResult> = {}) {
    super(message);
  }
}

/**
 * 以指定的 validations 验证 obj 是否为符合 validations 要求的 Record 结构。
 *
 * 复合型结构验证，可使用 callback 形式
 *
 * ```
 * withinFields(obj, {
 *   key: shouldNot(isEmptyString),
 *   data: (v) => withFieldsSilent(v, { name: 'string', code: 'string' }),
 * })
 * ```
 *
 * @param obj
 * @param validations 验证字段，可以是数组形式，表示字符串类型字段的验证，如 `['name', 'code']`，也可以是 object 结构的，`{ name: 'string', id: 'number' }`
 *
 * @throws {@link Error} 不是一个有效的 Record 对象
 * @throws {@link ValidationsError} 存在字段验证错误时
 */
export const withFields = <T = AnyObject>(
  obj: T,
  validations: ValidationsStringTypeArray<T> | ValidationsMap<T>,
): obj is T => {
  if (!isRecord<T>(obj)) throw new Error(i18n('notObject'));

  let errorCount = 0;
  const errorFields: Record<string, ValidateResult> = {};

  Object.entries(convertValidations(validations)).forEach(([field, type]) => {
    // @ts-ignore obj[field]
    const res = validate(field, obj[field] ?? undefined, type);
    if (res.code !== ValidateCode.positive) {
      errorCount += 1;
      errorFields[field] = res;
    }
  });

  if (errorCount > 0) throw new ValidationError(i18n('validationError'), errorFields);
  return true;
};
